/*
 *
 * Copyright (C) 2012 Walter Lütgenau
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
package offset.nodes.server.search.model;

import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.StringTokenizer;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.Workspace;
import javax.jcr.query.Query;
import javax.jcr.query.QueryManager;
import javax.jcr.query.QueryResult;
import offset.nodes.Constants;
import offset.nodes.server.icon.model.IconModel;
import offset.nodes.server.view.list.XPathParser;
import offset.nodes.server.model.RepositoryModel;
import offset.nodes.server.model.ServerNamespaceRegistry;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeDef;
import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
import org.apache.jackrabbit.core.nodetype.PropDef;
import org.apache.jackrabbit.util.ISO9075;

/**
 *
 * @author Walter Lütgenau
 */
public class SearchModel extends RepositoryModel {
    ServerNamespaceRegistry namespaces = new ServerNamespaceRegistry();
    public static final String REPOSITORY_ROOT = "/jcr:root";
    
    /** Creates a new instance of SearchModel */
    public SearchModel(Session session) {
        super(session);
    }
    
    public NodeIterator fullText(String search) throws RepositoryException {
        return executeTextQuery(search).getNodes();
    }

    public QueryResult executeTextQuery(String search) throws RepositoryException {
        Workspace workspace = getSession().getWorkspace();
        QueryManager manager = workspace.getQueryManager();
        String queryString = "/jcr:root//*[@jcr:primaryType != 'nt:frozenNode' and jcr:contains(.,'"+ search + "')] order by jcr:score() descending";
        
        Query query = manager.createQuery(queryString, Query.XPATH);
        QueryResult result = query.execute();
        
        return result;
    }

    protected String correctContainsGlitch(String search) {
        return search.replaceAll("jcr:contains\\((.*)/.*\\,", "jcr:contains($1/.,");
    }
    
    public QueryResult executeXPathQuery(String search) throws RepositoryException {
        Workspace workspace = getSession().getWorkspace();
        QueryManager manager = workspace.getQueryManager();
        
        search = encode9075(search);

        if (search.startsWith(Constants.PATH_SEPARATOR))
            search = REPOSITORY_ROOT + search;

        Query query = manager.createQuery(correctContainsGlitch(search), Query.XPATH);
        QueryResult result = query.execute();

        return result;
    }
    
    /**
     * Substitute the path component by its ISO 9075 representation.
     * 
     * @param query the unmodified query
     * @return the ISO 9075 representation.
     */
    public String encode9075(String query) {
        XPathParser parser = new XPathParser(query);
        String path = parser.getPath();
        
        int start = path.length();
        
        path = ISO9075.encodePath(path);
        if (start < query.length()) {
            return path + query.substring(start);
        }
        
        return path;
    }

   public QueryResult executeXPathQuery(String path, String filter) throws RepositoryException {
       return executeXPathQuery(escapeXMLPath(path) + filter);
    }

    public QueryResult executeSql1Query(String search) throws RepositoryException {
        Workspace workspace = getSession().getWorkspace();
        QueryManager manager = workspace.getQueryManager();
        
        Query query = manager.createQuery(search, Query.SQL);
        QueryResult result = query.execute();
        
        return result;
    }
    
    protected Collection<PropDef> getInheritedPropDefs(NodeTypeManagerImpl nodeTypeManager, NodeTypeDef nodeTypeDefinition) throws RepositoryException {
        LinkedList<PropDef> propDefs = new LinkedList<PropDef>();
        while( nodeTypeDefinition != null) {
            for( PropDef propertyDefinition: nodeTypeDefinition.getPropertyDefs())
                propDefs.add(propertyDefinition);
            if ( nodeTypeDefinition.getSupertypes() != null && nodeTypeDefinition.getSupertypes().length > 0)
                nodeTypeDefinition = nodeTypeManager.getNodeTypeRegistry().getNodeTypeDef(nodeTypeDefinition.getSupertypes()[0]);
            else
                nodeTypeDefinition = null;
        }
        
        return propDefs;
    }

    protected Collection<PropDef> getPropDefs(String xpathQuery) throws RepositoryException {
        HashSet<String>result = new HashSet<String>();
        XPathParser parser = new XPathParser(xpathQuery);
        String nodeType = parser.getTypeConstraint();
        if ( nodeType == null)
            return null;

        SessionImpl s = (SessionImpl) getSession();
        NodeTypeManagerImpl nmgr = s.getNodeTypeManager();
        NodeTypeDef nodeTypeDefinition = nmgr.getNodeTypeRegistry().getNodeTypeDef(namespaces.toName(nodeType));
        Collection<PropDef> propDefs = getInheritedPropDefs(nmgr, nodeTypeDefinition );

        return propDefs;
    }

    /**
     * Return the columns, which correspond to properties of reference type.
     * 
     * @param xpathQuery
     * @return
     * @throws RepositoryException
     */
    public HashSet<String> getReferenceColumns(String xpathQuery) throws RepositoryException {
        HashSet<String>result = new HashSet<String>();
        Collection<PropDef> propDefs = getPropDefs(xpathQuery);
        for(PropDef propertyDefinition: propDefs) {
            if ( propertyDefinition.getRequiredType() == PropertyType.REFERENCE)
                result.add(namespaces.toPrefixName(propertyDefinition.getName()));
        }

        return result;
    }

    /**
     * Return the columns, that correspond to properties with enumerated values, if they may be
     * displayed using icons.
     *
     * These are columns, for which a corresponding property value icon has been stored
     * in the appropriate location.
     *
     * @param xpathQuery
     * @return
     * @throws RepositoryException
     */
    public HashMap<String,String> getEnumeratedColumns(String xpathQuery) throws RepositoryException {
        HashMap<String,String>result = new HashMap<String,String>();
        XPathParser parser = new XPathParser(xpathQuery);
        String nodeType = parser.getTypeConstraint();
        if ( nodeType == null)
            return result;

        Collection<PropDef> propDefs = getPropDefs(xpathQuery);
        for(PropDef propertyDefinition: propDefs) {
            if ( propertyDefinition.getRequiredType() == PropertyType.STRING
                    && propertyDefinition.getValueConstraints() != null
                    && propertyDefinition.getValueConstraints().length > 0) {
                IconModel iconModel = new IconModel(getSession());
                if (iconModel.hasPropertyValueIcon(nodeType, namespaces.toPrefixName(propertyDefinition.getName()), propertyDefinition.getValueConstraints()[0].getDefinition()))
                    result.put(namespaces.toPrefixName(propertyDefinition.getName()),
                            nodeType);
            }
        }

        return result;
    }

    /**
     * Return the columns, that correspond to properties of BOOLEAN type.
     *
     * @param xpathQuery
     * @return
     * @throws RepositoryException
     */
    public HashMap<String,Integer> getBooleanColumns(String xpathQuery) throws RepositoryException {
        HashMap<String,Integer>result = new HashMap<String,Integer>();
        XPathParser parser = new XPathParser(xpathQuery);
        String nodeType = parser.getTypeConstraint();
        if ( nodeType == null)
            return result;

        Collection<PropDef> propDefs = getPropDefs(xpathQuery);
        for(PropDef propertyDefinition: propDefs) {
            if (propertyDefinition.getRequiredType() == PropertyType.BOOLEAN) {
                IconModel iconModel = new IconModel(getSession());
                if (iconModel.hasBooleanValueIcons())
                    result.put(namespaces.toPrefixName(propertyDefinition.getName()), new Integer(PropertyType.BOOLEAN));
            }
        }

        return result;
    }

    protected String escapeXMLPath(String path) {
        StringTokenizer parser = new StringTokenizer(path, "/[", true);
        StringBuffer buf = new StringBuffer();

        boolean attribute = false;
        while (parser.hasMoreTokens()) {
            String name = parser.nextToken();
            if (name.equals("["))
                attribute = true;
            if (name.equals("/"))
                attribute = false;
            else if (!name.equals("/") && !name.equals("*") && !attribute)
                name = escapeXMLName(name);

            buf.append(name);
        }

        return buf.toString();
    }

    protected String escapeXMLName(String name) {
        StringBuffer buf = new StringBuffer();

        for (int i = 0; i < name.length(); i++) {
            String newChar;

            if (i == 0 && !isFirstNameChar(name.charAt(i)))
                newChar = escapeChar(name.charAt(i));
            else if (name.charAt(i) == '[') {
                int index = name.indexOf("]", i);
                if (index > 0) {
                    newChar = name.substring(i, index + 1);
                    i += newChar.length();
                } else
                    newChar = "" + name.charAt(i);
            } else if (name.charAt(i) == '_' && startsWithEscape(name.substring(i)))
                newChar = escapeChar(name.charAt(i));
            else if (!isNameChar(name.charAt(i)))
                newChar = escapeChar(name.charAt(i));
            else
                newChar = "" + name.charAt(i);

            buf.append(newChar);
        }

        return buf.toString();
    }

    protected boolean isFirstNameChar(char first) {
        return Character.isLetter(first) || first == '_' || first == ':';
    }

    protected boolean isNameChar(char c) {
        return Character.isLetter(c) || Character.isDigit(c) || c == '.' || c == '-' || c == '_' || c == ':';
    }

    protected String escapeChar(char c) {
        return "_x00" + Integer.toHexString((int) c) + "_";
    }

    protected boolean isHexChar(char c) {
        return Character.isDigit(c) || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f');
    }

    protected boolean startsWithEscape(String name) {
        if (name.length() < 5)
            return false;

        if (!name.startsWith("_x"))
            return false;

        for (int i = 0; i < 4; i++) {
            if (!isHexChar(name.charAt(i + 2)))
                return false;
        }

        return true;
    }



    public static void main(String[] arguments) {
        String test = "//element(*, nodes:task) [jcr:contains(nodes:description/nodes:text,'version history')]/(@jcr:path)" ;
        System.out.println(test.replaceAll("jcr:contains\\((.*)/.*\\,", "jcr:contains($1/.,"));
    }

}
